package com.sharelords.biz.creator.memory;

import com.sharelords.biz.constant.Constant;
import com.sharelords.biz.system.ApplicationHome;
import com.sun.tools.javac.file.JavacFileManager;
import org.springframework.boot.loader.jar.JarFile;

import javax.tools.*;
import java.io.*;
import java.net.URI;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.logging.Logger;

/**
 * @author 千古龙少
 * @Description: 内存Java文件管理器
 * @date 2020年6月14日 下午8:35:26
 */
class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    private static final Logger logger = Logger.getLogger("MemoryJavaFileManager");

    private final Map<String, byte[]> classBytes = new HashMap<>();

    private final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>();

    private JavacFileManager javaFileManager;

    /**
     * key 包名 value javaObj 主要给jdk编译class的时候找依赖class用
     */
    private static final Map<String, List<JavaFileObject>> CLASS_OBJECT_PACKAGE_MAP = new HashMap<>();

    private static final Object LOCK = new Object();

    private boolean isInit = false;

    private String currentProjectPath;

    private boolean isJar;

    MemoryJavaFileManager(JavaFileManager fileManager) {
        super(fileManager);
        this.javaFileManager = (JavacFileManager) fileManager;
        currentProjectPath = this.getCurrentPath();
        isJar = currentProjectPath.endsWith(".jar");
    }

    Map<String, byte[]> getClassBytes() {
        return new HashMap<>(this.classBytes);
    }

    /**
     * 获取当前项目路径
     *
     * @return 当前项目路径
     */
    private String getCurrentPath() {
        ApplicationHome home = new ApplicationHome(MemoryJavaFileManager.class);
        File jarFile = home.getSource();
        if (jarFile == null) {
            return "";
        }

        return jarFile.getPath();
    }

    @Override
    public void flush() {
    }

    @Override
    public void close() {
        classBytes.clear();
    }

    @Override
    public String inferBinaryName(Location location, JavaFileObject file) {
        if (file instanceof MemoryInputJavaClassObject) {
            return ((MemoryInputJavaClassObject) file).inferBinaryName();
        }

        return super.inferBinaryName(location, file);
    }

    @Override
    public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location,
                                               String className,
                                               JavaFileObject.Kind kind,
                                               FileObject sibling) throws IOException {
        if (kind == JavaFileObject.Kind.CLASS) {
            return new MemoryOutputJavaClassObject(className);
        }

        return super.getJavaFileForOutput(location, className, kind, sibling);
    }

    @Override
    public Iterable<JavaFileObject> list(Location location,
                                         String packageName,
                                         Set<JavaFileObject.Kind> kinds,
                                         boolean recurse) throws IOException {
        if (Constant.CLASS_PATH.equals(location.getName()) && isJar) {
            List<JavaFileObject> result = getLibJarsOptions(packageName);
            if (result != null) {
                return result;
            }
        }

        Iterable<JavaFileObject> it = super.list(location, packageName, kinds, recurse);
        if (!kinds.contains(JavaFileObject.Kind.CLASS)) {
            return it;
        }

        final List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
        if (javaFileObjectList == null) {
            return it;
        }

        if (it != null) {
            for (JavaFileObject javaFileObject : it) {
                javaFileObjectList.add(javaFileObject);
            }
        }

        return javaFileObjectList;
    }

    private List<JavaFileObject> getLibJarsOptions(String packageName) {
        synchronized (LOCK) {
            if (!isInit) {
                init();
            }
        }
        return CLASS_OBJECT_PACKAGE_MAP.get(packageName);
    }

    private void init() {
        try {
            JarFile jarFile = new JarFile(new File(currentProjectPath));
            Enumeration<JarEntry> entries = jarFile.entries();
            JarFile libTempJarFile;
            List<JavaFileObject> onePackageJavaFiles;
            String packageName;
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().endsWith(".jar")) {
                    libTempJarFile = jarFile.getNestedJarFile(jarFile.getEntry(entry.getName()));
                    if (!libTempJarFile.getName().contains("tools.jar")) {
                        Enumeration<JarEntry> tempEntriesEnum = libTempJarFile.entries();
                        while (tempEntriesEnum.hasMoreElements()) {
                            JarEntry jarEntry = tempEntriesEnum.nextElement();
                            String classPath = jarEntry.getName().replace("/", ".");
                            if (classPath.endsWith(".class") && jarEntry.getName().lastIndexOf('/') != -1) {
                                packageName = classPath.substring(0, jarEntry.getName().lastIndexOf('/'));
                                onePackageJavaFiles = CLASS_OBJECT_PACKAGE_MAP.containsKey(packageName) ? CLASS_OBJECT_PACKAGE_MAP.get(packageName) : new ArrayList<JavaFileObject>();
                                onePackageJavaFiles.add(new MemorySpringBootInfoJavaClassObject(jarEntry.getName().replace("/", ".").replace(".class", ""),
                                        new URL(libTempJarFile.getUrl(), jarEntry.getName()), javaFileManager));
                                CLASS_OBJECT_PACKAGE_MAP.put(packageName, onePackageJavaFiles);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        isInit = true;
    }

    private static class MemoryInputJavaClassObject extends SimpleJavaFileObject {
        final String className;
        final byte[] bs;

        MemoryInputJavaClassObject(String className, byte[] bs) {
            super(URI.create(Constant.STRING_FLAG + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
            this.className = className;
            this.bs = bs;
        }

        @Override
        public InputStream openInputStream() {
            return new ByteArrayInputStream(bs);
        }

        String inferBinaryName() {
            return className;
        }
    }

    private class MemoryOutputJavaClassObject extends SimpleJavaFileObject {
        final String className;

        MemoryOutputJavaClassObject(String className) {
            super(URI.create(Constant.STRING_FLAG + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
            this.className = className;
        }

        @Override
        public OutputStream openOutputStream() {
            return new FilterOutputStream(new ByteArrayOutputStream()) {
                @Override
                public void close() throws IOException {
                    out.close();
                    ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
                    byte[] bs = bos.toByteArray();
                    classBytes.put(className, bs);
                    makeBinaryClass(className, bs);
                }
            };
        }

        private void makeBinaryClass(String className, final byte[] bs) {
            JavaFileObject javaFileObject = new MemoryInputJavaClassObject(className, bs);

            String packageName = "";
            int pos = className.lastIndexOf('.');
            if (pos > 0) {
                packageName = className.substring(0, pos);
            }

            List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
            if (javaFileObjectList != null) {
                javaFileObjectList.add(javaFileObject);
                return;
            }

            javaFileObjectList = new LinkedList<>();
            javaFileObjectList.add(javaFileObject);

            classObjectPackageMap.put(packageName, javaFileObjectList);
        }
    }

}